通过基础篇:后台同步可知,我们可通过后台同步机制来解决传统 Web 应用所存在的以下问题:

  • 页面发起的请求会随着页面的关闭而终止。
  • 在离线状态下,很难将用户的网络请求缓存起来,并在网络恢复正常后再次进行请求。
  • 从而为用户提供了恶劣网络环境下,无感知事务处理的能力。在该章中我们已对底层 API 的使用进行了详细介绍,故本章不再重述相关细节,而是直接对 Workbox 相关接口进行介绍说明。

# 基本使用

Workbox 使用 workbox.backgroundSync.Plugin 完成后台同步的注册,比如:

workbox.routing.registerRoute(
  '/articles',
  new workbox.strategies.NetworkOnly({
    plugins: [
      new workbox.backgroundSync.Plugin('createArticle')
    ]
  }),
  'POST'
);

示例中,当 POST /articles 请求失败时,Workbox 会自动将该请求添加到后台同步队列 createArticle 中,并在 sync 事件中自动进行重试。其中 workbox.backgroundSync.Plugin的参数按照顺序依次为:

  • name:队列名称,该参数的值必须全局唯一,否则将抛出 duplicate-queue-name 异常。
  • options: 配置信息,相关属性为
    • maxRetentionTime:请求的最大有效时长(单位为分钟,默认值为七天),如果请求超过所指定的期限,将从队列中移除。
    • onSyncsync 触发时的回调函数,该回调函数的参数为含有 queue(类型为 workbox.backgroundSync.Queue 实例)属性的对象,如不指定将调用 workbox.backgroundSync.Queue 实例的 replayRequests 方法。若指定该属性的值,如果回调函数处理失败,需要抛出异常,以便浏览器后续继续进行尝试。

由于 workbox.backgroundSync.Plugin内部使用了 workbox.backgroundSync.Queue来管理同步请求队列,因此我们可以使用它来自行控制请求被加入队列的时机,比如

const queue = new workbox.backgroundSync.Queue('createArticle');

self.addEventListener('fetch', event => {
  event.waitUntil(
    fetch(event.request.clone).catch(() => {
      return queue.pushRequest({ request: event.request });
    })
  );
});

示例中,我们首先定义了 workbox.backgroundSync.Queue 实例 queue,然后在 fetch 事件中,如果请求出现异常,则调用 queue 的 pushRequest 方法将相关请求添加到同步队列中,之后 sync 事件触发后将自动重试队列中的请求。由于 workbox.backgroundSync.Queue 的参数与 workbox.backgroundSync.Plugin 一致,故不再重述。

# 同步事件注册

为了启用后台同步,我们在页面中注册了同步事件,主要代码如下:

window.addEventListener('load', function() {
  if ('serviceWorker' in navigator && 'SyncManager' in window) {
    navigator.serviceWorker.register('./sw.js').then(function(registration) {
      document.getElementById('submit').addEventListener('click', function() {
        ui.submit(function(name) {
          db.addTodo(name).then(function() {
            registration.sync.register('add-todo');
          });
        });
      });
    });
  } else {
    document.getElementById('submit').addEventListener('click', function() {
      ui.submit(function(name) {
        network.addTodos([{ name: name }]).then(function(todos) {
          ui.render(todos);
        });
      });
    });
  }
});

上例主要存在以下问题:

  • 为了兼容不支持后台同步的浏览器,我们必须要对每一个网络请求做兼容性处理;
  • 由于 Service Worker 线程无法直接访问 DOM,故需先将请求参数缓存在 IndexedDB 中,以便 Service Worker 在 sync 事件中能够成功构建相关请求。这便要求在页面主线程与 Service Worker 线程中所使用 IndexedDB 数据结构必须保持一致,如果一方更新了结构,而另一方尚未更新,则将造成意想不到的问题;
  • 也由于后台同步只有在网络发生异常的情况下才能体现出其价值,但对处于稳定网络环境下的用户来说,在真正发起请求之前还要等待:缓存请求参数、注册同步事件、触发 sync 事件等一系列事件,这些不必要的开销累积起来很可能会严重影响用户体验;
  • 最后,也是最重要的一点,PWA 所提倡的是以渐进、尽量少入侵的方式来为已有的 Web 应用添加离线处理等各种能力,如在页面中注册同步事件,将要大面积修改代码,明显违背了 PWA 的初衷。

基于以上原因,在 Workbox 中,无论是使用 workbox.backgroundSync.Plugin,还是 workbox.backgroundSync.Queue,我们都无需对页面代码进行任何修改,因此上例代码可简化为:

window.addEventListener('load', function() {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('./sw.js');
  }
  document.getElementById('submit').addEventListener('click', function() {
    ui.submit(function(name) {
      network.addTodos([{ name: name }]).then(function(todos) {
        ui.render(todos);
      });
    });
  });
});

这是因为 Workbox 在将请求加入同步队列时自动完成了同步事件的注册,如果我们使用 workbox.backgroundSync.Plugin 中仅在请求失败时将其加入同步队列的策略,这即能保证稳定网络下的高效率,又能解决网络中断又恢复后的自动重试问题。因此,无论是否使用 Workbox 的后台同步接口,尽量在 Service Worker 线程中,并且在请求异常时注册同步事件。

# 总结

本章我们首先对 Workbox 后台同步接口进行了介绍,然后简单讨论了同步事件注册的时机问题,通过 Workbox,我们无需修改页面逻辑便可为已有 Web 应用添加离线事务处理的能力,这也进一步体现了 PWA 中的渐进式思想。下一章,我们将对 Workbox 的插件进行讨论。

阅读全文